{ $HDR$}
{**********************************************************************}
{ Unit archived using Team Coherence                                   }
{ Team Coherence is Copyright 2002 by Quality Software Components      }
{                                                                      }
{ For further information / comments, visit our WEB site at            }
{ http://www.TeamCoherence.com                                         }
{**********************************************************************}
{}
{ $Log:  10257: IdMessageCoderMIME.pas 
{
{   Rev 1.11    8/15/04 5:25:12 PM  RLebeau
{ Rewrote ReadHeader() to handle attachments similar to how Indy 10 does now
}
{
{   Rev 1.10    8/10/04 1:28:18 PM  RLebeau
{ Updated TIdMessageDecoderMIME to support multi-part form data
}
{
{   Rev 1.9    6/4/04 12:38:34 PM  RLebeau
{ ContentTransferEncoding bug fix
}
{
{   Rev 1.8    5/28/04 12:18:42 PM  RLebeau
{ Fix for compiler error
}
{
{   Rev 1.7    25/05/2004 13:57:12  CCostelloe
{ Bug fix
}
{
{   Rev 1.6    5/1/04 3:04:52 AM  RLebeau
{ Updated TIdMessageDecoderInfoMIME.CheckForStart() to return nil if no
{ boundary is specified in the message
}
{
{   Rev 1.5    2003.09.04 5:42:50 PM  czhower
{ Update to produce lower SpamAsassin scores.
}
{
    Rev 1.4    6/14/2003 10:40:36 AM  BGooijen
  fix for the bug where the attachments are empty
}
{
{   Rev 1.2    5/23/03 9:51:04 AM  RLebeau
{ Minor tweak to previous fix.
}
{
{   Rev 1.1    5/23/03 9:43:12 AM  RLebeau
{ Fixed bugs where message body is parsed incorrectly when MIMEBoundary is
{ empty.
}
{
{   Rev 1.0    2002.11.12 10:46:04 PM  czhower
}
unit IdMessageCoderMIME;

// for all 3 to 4s:
//// TODO: Predict output sizes and presize outputs, then use move on
// presized outputs when possible, or presize only and reposition if stream

interface

uses
  Classes,
  IdMessageCoder, IdMessage;

type
  TIdMessageDecoderMIME = class(TIdMessageDecoder)
  protected
    FFirstLine: string;
    FBodyEncoded: Boolean;
    FMIMEBoundary: string;
  public
    constructor Create(AOwner: TComponent); reintroduce; overload;
    constructor Create(AOwner: TComponent; ALine: string); reintroduce; overload;
    function ReadBody(ADestStream: TStream;
      var VMsgEnd: Boolean): TIdMessageDecoder; override;
    procedure ReadHeader; override;
    //
    property MIMEBoundary: string read FMIMEBoundary write FMIMEBoundary;
    property BodyEncoded: Boolean read FBodyEncoded write FBodyEncoded;
  end;

  TIdMessageDecoderInfoMIME = class(TIdMessageDecoderInfo)
  public
    function CheckForStart(ASender: TIdMessage; ALine: string): TIdMessageDecoder; override;
  end;

  TIdMessageEncoderMIME = class(TIdMessageEncoder)
  public
    procedure Encode(ASrc: TStream; ADest: TStream); override;
  end;

  TIdMessageEncoderInfoMIME = class(TIdMessageEncoderInfo)
  public
    constructor Create; override;
    procedure InitializeHeaders(AMsg: TIdMessage); override;
  end;

const
  IndyMIMEBoundary = '=_MoreStuf_2zzz1234sadvnqw3nerasdf'; {do not localize}
  IndyMultiPartAlternativeBoundary = '=_MoreStuf_2altzzz1234sadvnqw3nerasdf'; {do not localize}
  IndyMultiPartRelatedBoundary = '=_MoreStuf_2relzzzsadvnq1234w3nerasdf'; {do not localize}
  MIMEGenericText = 'text/'; {do not localize}
  MIMEGenericMultiPart = 'multipart/'; {do not localize}
  MIME7Bit = '7bit'; {do not localize}

implementation

uses
  IdCoder, IdCoderMIME, IdException, IdGlobal, IdResourceStrings, IdCoderQuotedPrintable,
  SysUtils, IdCoderHeader;

{ TIdMessageDecoderInfoMIME }

function TIdMessageDecoderInfoMIME.CheckForStart(ASender: TIdMessage;
 ALine: string): TIdMessageDecoder;
begin
  if (ASender.MIMEBoundary.Boundary <> '') then begin
    if AnsiSameText(ALine, '--' + ASender.MIMEBoundary.Boundary) then begin    {Do not Localize}
      Result := TIdMessageDecoderMIME.Create(ASender);
    end else if AnsiSameText(ASender.ContentTransferEncoding, 'base64') or    {Do not Localize}
      AnsiSameText(ASender.ContentTransferEncoding, 'quoted-printable') then begin    {Do not Localize}
        Result := TIdMessageDecoderMIME.Create(ASender, ALine);
    end else begin
      Result := nil;
    end;
  end else begin
    Result := nil;
  end;
end;

{ TIdCoderMIME }

constructor TIdMessageDecoderMIME.Create(AOwner: TComponent);
begin
  inherited;
  FBodyEncoded := False;
  if AOwner is TIdMessage then begin
    FMIMEBoundary := TIdMessage(AOwner).MIMEBoundary.Boundary;
    if (TIdMessage(AOwner).ContentTransferEncoding <> '') and
      (not AnsiSameText(TIdMessage(AOwner).ContentTransferEncoding, '7bit')) and
      (not AnsiSameText(TIdMessage(AOwner).ContentTransferEncoding, '8bit')) and
      (not AnsiSameText(TIdMessage(AOwner).ContentTransferEncoding, 'binary')) then
    begin
      FBodyEncoded := True;
    end;
  end;
end;

constructor TIdMessageDecoderMIME.Create(AOwner: TComponent; ALine: string);
begin
  Create(AOwner);
  FFirstLine := ALine;
end;

function TIdMessageDecoderMIME.ReadBody(ADestStream: TStream; var VMsgEnd: Boolean): TIdMessageDecoder;
var
  s: string;
  LDecoder: TIdDecoder;
  LLine: string;
begin
  VMsgEnd := FALSE;
  Result := nil;
  if FBodyEncoded then begin
    s := TIdMessage(Owner).ContentTransferEncoding;
  end else begin
    s := FHeaders.Values['Content-Transfer-Encoding']; {Do not Localize}
  end;
  if AnsiSameText(s, 'base64') then begin {Do not Localize}
    LDecoder := TIdDecoderMIME.Create(nil);
  end else if AnsiSameText(s, 'quoted-printable') then begin {Do not Localize}
    LDecoder := TIdDecoderQuotedPrintable.Create(nil);
  end else begin
    LDecoder := nil;
  end;
  try
    repeat
      if FFirstLine = '' then begin // TODO: Improve this. Not very efficient
        LLine := ReadLn;
      end else begin
        LLine := FFirstLine;
        FFirstLine := '';    {Do not Localize}
      end;
      if LLine = '.' then begin // Do not use ADELIM since always ends with . (standard) {Do not Localize}
        VMsgEnd := True;
        Break;
      end;
      // New boundary - end self and create new coder
      if MIMEBoundary <> '' then begin
        if AnsiSameText(LLine, '--' + MIMEBoundary) then begin    {Do not Localize}
          Result := TIdMessageDecoderMIME.Create(Owner);
          Break;
        // End of all coders (not quite ALL coders)
        end
        else if AnsiSameText(LLine, '--' + MIMEBoundary + '--') then begin    {Do not Localize}
          // POP the boundary
          if Owner is TIdMessage then begin
            TIdMessage(Owner).MIMEBoundary.Pop;
          end;
          Break;
        // Data to save, but not decode
        end else if LDecoder = nil then begin
          if (Length(LLine) > 0) and (LLine[1] = '.') then begin // Process . in front for no encoding    {Do not Localize}
            Delete(LLine, 1, 1);
          end;
          LLine := LLine + EOL;
          ADestStream.WriteBuffer(LLine[1], Length(LLine));
        // Data to decode
        end else begin
          //for TIdDecoderQuotedPrintable, we have
          //to make sure all EOLs are intact
          if LDecoder is TIdDecoderQuotedPrintable then begin
            LDecoder.DecodeToStream(LLine+EOL,ADestStream);
          end else if LLine <> '' then begin
            LDecoder.DecodeToStream(LLine, ADestStream);
          end;
        end;
      end;
    until False;
  finally FreeAndNil(LDecoder); end;
end;

procedure TIdMessageDecoderMIME.ReadHeader;
var
  ABoundary,
  s: string;
  LLine: string;

  function GetAttachmentFilename(AContentType, AContentDisposition: string): string;
  var
    LValue: string;
    LPos: Cardinal;
  begin
    LPos := IndyPos('FILENAME=', UpperCase(AContentDisposition));  {Do not Localize}
    if LPos > 0 then begin
      LValue := Trim(Copy(AContentDisposition, LPos + 9, MaxInt));
    end else begin
      LValue := ''; //FileName not found
    end;
    if Length(LValue) = 0 then begin
      // Get filename from Content-Type
      LPos := IndyPos('NAME=', UpperCase(AContentType)); {Do not Localize}
      if LPos > 0 then begin
        LValue := Trim(Copy(AContentType, LPos + 5, MaxInt));    {Do not Localize}
      end;
    end;
    if Length(LValue) > 0 then begin
      if LValue[1] = '"' then begin        {Do not Localize}
        // RLebeau - shouldn't this code use AnsiExtractQuotedStr() instead?
        Fetch(LValue, '"');                {Do not Localize}
        Result := Fetch(LValue, '"');      {Do not Localize}
      end else begin
        // RLebeau - just in case the name is not the last field in the line
        Result := Fetch(LValue, ';');      {Do not Localize}
      end;
      Result := DecodeHeader(Result);
    end else begin
      Result := '';
    end;
  end;

  procedure CheckAndSetType(AContentType, AContentDisposition: string);
  var
    LDisposition, LFileName: string;
  begin
    LDisposition := Fetch(AContentDisposition, ';');            {Do not Localize}

    {The new world order: Indy now defines a TIdAttachment as a part that either has
    a filename, or else does NOT have a ContentType starting with text/ or multipart/.
    Anything left is a TIdText.}

    //WARNING: Attachments may not necessarily have filenames!
    LFileName := GetAttachmentFileName(AContentType, AContentDisposition);

    // Content-Disposition: inline; - Even this we treat as attachment. It
    // can easily contain binary data which text part is not suited for.
    if AnsiSameText(LDisposition, 'attachment') or (Length(LFileName) > 0) then {Do not Localize}
    begin
      FPartType := mcptAttachment;
      FFilename := LFileName;
    end else begin
      {No filename is specified, so see what type the part is...}
      if AnsiSameText(Copy(AContentType, 1, 5), MIMEGenericText) or           
        AnsiSameText(Copy(AContentType, 1, 10), MIMEGenericMultiPart) then
      begin
        FPartType := mcptText;
      end else begin
        FPartType := mcptAttachment;
      end;
    end;
  end;

begin
  if FBodyEncoded then begin // Read header from the actual message since body parts don't exist    {Do not Localize}
    CheckAndSetType(TIdMessage(Owner).ContentType, TIdMessage(OWner).ContentDisposition);
  end else begin
    // Read header
    repeat
      LLine := ReadLn;
      if LLine = '.' then begin // TODO: abnormal situation (Masters!)    {Do not Localize}
        FPartType := mcptUnknown;
        Exit;
      end;//if
      if LLine = '' then begin
        Break;
      end;
      if LLine[1] in LWS then begin
        if FHeaders.Count > 0 then begin
          FHeaders[FHeaders.Count - 1] := FHeaders[FHeaders.Count - 1] + ' ' + Copy(LLine, 2, MaxInt);    {Do not Localize}
        end else begin
          FHeaders.Add(StringReplace(Copy(LLine, 2, MaxInt), ': ', '=', [])); {Do not Localize}
        end;
      end else begin
        FHeaders.Add(StringReplace(LLine, ': ', '=', []));    {Do not Localize}
      end;
    until False;
    s := FHeaders.Values['Content-Type'];    {Do not Localize}
    ABoundary := TIdMIMEBoundary.FindBoundary(s);
    if Length(ABoundary) > 0 then begin
      if Owner is TIdMessage then begin
        TIdMessage(Owner).MIMEBoundary.Push(ABoundary);
        // Also update current boundary
        FMIMEBoundary := ABoundary;
      end;
    end;
    CheckAndSetType(FHeaders.Values['Content-Type'],    {Do not Localize}
      FHeaders.Values['Content-Disposition']);    {Do not Localize}
  end;
end;

{ TIdMessageEncoderInfoMIME }

constructor TIdMessageEncoderInfoMIME.Create;
begin
  inherited;
  FMessageEncoderClass := TIdMessageEncoderMIME;
end;

procedure TIdMessageEncoderInfoMIME.InitializeHeaders(AMsg: TIdMessage);
begin
  if AMsg.MessageParts.RelatedPartCount > 0 then begin
    AMsg.ContentType := 'multipart/related; type="multipart/alternative"; boundary="' + {do not localize}
     IndyMultiPartRelatedBoundary + '"';    {Do not Localize}
  end else begin
    if AMsg.MessageParts.AttachmentCount > 0 then begin
      AMsg.ContentType := 'multipart/mixed; boundary="' {do not localize}
       + IndyMIMEBoundary + '"';    {Do not Localize}
    end else begin
      if AMsg.MessageParts.TextPartCount > 0 then begin
        AMsg.ContentType :=
         'multipart/alternative; boundary="' {do not localize}
         + IndyMIMEBoundary + '"';    {Do not Localize}
      end;
    end;
  end;
end;

{ TIdMessageEncoderMIME }

procedure TIdMessageEncoderMIME.Encode(ASrc, ADest: TStream);
var
  s: string;
  LEncoder: TIdEncoderMIME;
  LSPos, LSSize : Int64;
begin
  ASrc.Position := 0;
  LSPos := 0;
  LSSize := ASrc.Size;
  LEncoder := TIdEncoderMIME.Create(nil); try
    while LSPos < LSSize do begin
      s := LEncoder.Encode(ASrc, 57) + EOL;
      Inc(LSPos,57);
      ADest.WriteBuffer(s[1], Length(s));
    end;
  finally FreeAndNil(LEncoder); end;
end;

initialization
  TIdMessageDecoderList.RegisterDecoder('MIME',    {Do not Localize}
    TIdMessageDecoderInfoMIME.Create);
  TIdMessageEncoderList.RegisterEncoder('MIME',    {Do not Localize}
    TIdMessageEncoderInfoMIME.Create);
end.
